package com.signway.swmediaplayer;

import static com.signway.swmediaplayer.SWMediaManager.AUDIO;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.SystemClock;
import android.text.TextUtils;

import com.signway.swmediaplayer.utils.SWLog;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SWAudioManager {
    private SWLog swLog = new SWLog(SWAudioManager.class.getName(), true, true, false);

    private class AudioByteBuffer {
        int index;
        byte[] buffer;
        public int flags;
        long presentationTimeUs;
        long renderTimestampNs;
        public AudioByteBuffer(int index, byte[] buffer, int length, int flags, long presentationTimeUs, long renderTimestampNs) {
            this.index = index;
            this.buffer = new byte[length];
            for (int i = 0; i < length; i++) {
                this.buffer[i] = buffer[i];
            }
            this.presentationTimeUs = presentationTimeUs;
            this.renderTimestampNs = renderTimestampNs;
            this.flags = flags;
        }
    };

    private enum AudioCommStatus {
        AUDIO_COMM_STATUS_IDLE,
        AUDIO_COMM_STATUS_END,
        AUDIO_COMM_STATUS_ERROR,
        AUDIO_COMM_STATUS_INITIALIZED,
        AUDIO_COMM_STATUS_PREPARED,
        AUDIO_COMM_STATUS_PREPARING,
        AUDIO_COMM_STATUS_PREPARE_ERROR,
        AUDIO_COMM_STATUS_STARTED,
        AUDIO_COMM_STATUS_NEXT_STARTED,
        AUDIO_COMM_STATUS_CODEC_END,
        AUDIO_COMM_STATUS_RENDER_END,
        AUDIO_COMM_STATUS_PAUSED,
        AUDIO_COMM_STATUS_STOPPED,
        AUDIO_COMM_STATUS_PLAYBACK_COMPLETED,
    };

    private volatile AudioCommStatus mAudioCommStatus;
    private String mFilePath = null;
    private long mStartWhenNs = 0;
    private long mPauseTimeNs = 0;
    private long mCodecStartTimeUs = 0;
    private long mCodecLastTimeUs = 0;
    private long mDecodeOffsetTimestampUs = 64000;
    private final Queue<AudioByteBuffer> mAudioByteBufferQueue = new LinkedList<>();

    public SWAudioManager(SWMediaManager mediaManager) {
        swLog.logd("SWAudioManager()");
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_IDLE;
        audioCommThread = new Thread(audioCommRunnable);
        audioCommThread.start();
        audioPlayerThread = new Thread(audioPlayerRunnable);
        audioPlayerThread.start();
        Arrays.fill(mAudioTrackList, null);
    }

    private SWMediaManager.OnMediaRenderListener mOnMediaRenderListener = null;
    public synchronized void setOnMediaRenderListener(SWMediaManager.OnMediaRenderListener listener) {
        mOnMediaRenderListener = listener;
    }

    public void switchDataSource() {
        if (mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_STARTED) {
            swLog.logd("don't allow switchDataSource");
            return;
        }
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_NEXT_STARTED;
    }

    public synchronized void setStartWhenNs(long startWhenNs) {
        mStartWhenNs = startWhenNs;
        swLog.logi("setStartWhenNs " + mStartWhenNs);
    }

    public synchronized void setCodecStartTimeUs(long codecStartTimeUs) {
        mCodecStartTimeUs = codecStartTimeUs;
        swLog.logd("setCodecStartTimeUs " + codecStartTimeUs);
    }

    public synchronized void setDataSource(String path) throws IOException {
        if (TextUtils.isEmpty(path)) {
            throw new IOException("data source is empty");
        }
        if (mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_IDLE) {
            throw new IOException("the current state is not idle");
        }
        File file = new File(path);
        if (!(file.exists() && file.isFile() && file.canRead())) {
            throw new IOException("data source is error");
        }
        swLog.logd("setDataSource " + path);
        mFilePath = path;
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_INITIALIZED;
    }

    public synchronized void prepare() throws IOException, IllegalStateException, InterruptedException {
        if (mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_INITIALIZED) {
            swLog.loge("the current state is not initialized");
            return;
        }
        swLog.logd("prepare()");
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_PREPARING;
        while (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_PREPARING) {
            this.wait();
        }
        swLog.logd("prepare() finish.");
    }

    public synchronized void start() {
        swLog.logd("start()");
        switch (mAudioCommStatus) {
            case AUDIO_COMM_STATUS_PREPARED:
                mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_STARTED;
                break;
            case AUDIO_COMM_STATUS_PAUSED:
                mPauseTimeNs = System.nanoTime() - mPauseTimeNs;
                mStartWhenNs += mPauseTimeNs;
                mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_STARTED;
                break;
            case AUDIO_COMM_STATUS_PREPARE_ERROR:
                /* 声音无法播放时直接返回播放完成 */
                swLog.logi("Audio AUDIO_COMM_STATUS_PREPARE_ERROR");
                if (mOnMediaRenderListener != null) {
                    mOnMediaRenderListener.onError("PREPARE_ERROR");
                }
            default:
                swLog.loge("the current state is not prepared");
                break;
        }
        swLog.logd("start() finish.");
    }

    public synchronized void pause() {
        if (mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_STARTED &&
            mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_PREPARED) {
            return;
        }
        swLog.logd("pause()");
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_PAUSED;
        mPauseTimeNs = System.nanoTime();
    }

    public void release() throws InterruptedException {
        long start = SystemClock.elapsedRealtime();
        swLog.logd("audio release()");
        synchronized (SWAudioManager.this) {
            if (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_END) {
                return;
            }
            mOnMediaRenderListener = null;
            mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_END;
            audioCommThread.interrupt();
            audioPlayerThread.interrupt();
        }
        semaphore.acquireUninterruptibly(2);
        semaphore.release(2);
        swLog.logd("audio release() finish. " + (SystemClock.elapsedRealtime() - start) + "(ms)");
    }

    public synchronized void reset() {
        synchronized (mAudioByteBufferQueue) {
            mAudioByteBufferQueue.clear();
        }
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_IDLE;
    }

    public void resetWaitAndNoClear() throws InterruptedException {
        long start = SystemClock.elapsedRealtime();
        while (true) {
            synchronized (SWAudioManager.this) {
                if (mAudioCommStatus != AudioCommStatus.AUDIO_COMM_STATUS_CODEC_END) {
                    break;
                }
            }
            TimeUnit.MILLISECONDS.sleep(1);
        }
        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_IDLE;
        swLog.logd("audio resetWaitAndNoClear() finish. " + (SystemClock.elapsedRealtime() - start) + "(ms)");
    }

    private Semaphore semaphore = new Semaphore(2);
    private Runnable audioPlayerRunnable = () -> {
        long sleepTime = 5;
        swLog.logi("audioPlayerRunnable start.");
        try {
            semaphore.acquire();
            swLog.logi("trackWriteProgress start.");
            while (!Thread.interrupted()) {
                synchronized (mAudioByteBufferQueue) {
                    trackWrite();
                }
                TimeUnit.MILLISECONDS.sleep(sleepTime);
            }
            swLog.logi("trackWriteProgress end.");
        } catch (InterruptedException e) {
            swLog.loge("audioPlayerRunnable interrupt = " + e);
        } catch (Exception e) {
            e.printStackTrace();
            swLog.loge("audioPlayerRunnable interrupt = " + e);
        } finally {
            synchronized (mAudioByteBufferQueue) {
                mAudioByteBufferQueue.clear();
            }
            semaphore.release();
        }
        swLog.logi("audioPlayerRunnable end.");
    };

    private Runnable audioCommRunnable = () -> {
        long sleepTime = 20;
        swLog.logi("audioCommRunnable start.");
        try {
            semaphore.acquire();
            swLog.logi("mainThreadProgress start.");
            while (!Thread.interrupted()) {
                mainThreadProgress();
                synchronized (SWAudioManager.this) {
                    if (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_END ||
                            mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_STOPPED) {
                        break;
                    }
                    if (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_STARTED ||
                            mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_CODEC_END) {
                        sleepTime = 5;
                    }
                    if (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_ERROR) {
                        if (mOnMediaRenderListener != null) {
                            mOnMediaRenderListener.onError("");
                        }
                    }
                }
                TimeUnit.MILLISECONDS.sleep(sleepTime);
            }
            swLog.logi("mainThreadProgress end.");
        } catch (InterruptedException e) {
            swLog.loge("audioCommRunnable interrupt = " + e);
        } catch (Exception e) {
            e.printStackTrace();
            swLog.loge("audioCommRunnable interrupt = " + e);
        } finally {
            audioCodecRelease(true);
            semaphore.release();
        }
        swLog.logi("audioCommRunnable end.");
    };

    private Thread audioCommThread = null;
    private Thread audioPlayerThread = null;
    private MediaExtractor mAudioExtractor = null;
    private MediaCodec mAudioDecoder = null;
    private int mInputBufferSize = 0;
    private int mAudioTrackIndex = 0;
    private AudioTrack[] mAudioTrackList = new AudioTrack[2];
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    ByteBuffer[] inputBuffers = null;
    ByteBuffer[] outputBuffers = null;
    byte[] mAudioOutBuffers = null;

    private void audioCodecRelease(boolean releaseAll) {
        swLog.logi("release start.");

        try {
            if (mAudioDecoder != null) {
                mAudioDecoder.stop();
                mAudioDecoder.release();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } finally {
            mAudioDecoder = null;
        }

        try {
            if (mAudioExtractor != null) {
                mAudioExtractor.release();
            }
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } finally {
            mAudioExtractor = null;
        }

        if (releaseAll) {
            for (int i = 0; i < mAudioTrackList.length; i++) {
                AudioTrack mAudioTrack = mAudioTrackList[i];
                try {
                    if (mAudioTrack != null) {
                        mAudioTrack.stop();
                        mAudioTrack.release();
                    }
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                } finally {
                    mAudioTrackList[i] = null;
                }
            }
        } else {
            AudioTrack mAudioTrack = mAudioTrackList[mAudioTrackIndex];
            try {
                if (mAudioTrack != null) {
                    mAudioTrack.stop();
                    mAudioTrack.release();
                }
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } finally {
                mAudioTrackList[mAudioTrackIndex] = null;
            }
        }

        swLog.logi("release end.");
    }

    private boolean audioCodecInit() throws Exception {
        mAudioTrackIndex++;
        mAudioTrackIndex %= mAudioTrackList.length;
        audioCodecRelease(false);
        mAudioExtractor = new MediaExtractor();
        mAudioExtractor.setDataSource(mFilePath);
        swLog.logi("setDataSource " + mFilePath);
        swLog.logi(mFilePath + ", getTrackCount: " + mAudioExtractor.getTrackCount());
        for (int i = 0; i < mAudioExtractor.getTrackCount(); i++) {
            MediaFormat format = mAudioExtractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith(AUDIO)) {
                swLog.logi("format : " + format + ", mime: " + mime);
                mAudioExtractor.selectTrack(i);
                int audioChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                int audioSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                int minBufferSize = AudioTrack.getMinBufferSize(audioSampleRate,
                        (audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
                        AudioFormat.ENCODING_PCM_16BIT);
                int maxInputSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
                mInputBufferSize = minBufferSize > 0 ? minBufferSize : maxInputSize;
                int frameSizeInBytes = audioChannels * 2;
                mInputBufferSize = (mInputBufferSize / frameSizeInBytes) * frameSizeInBytes;
                mAudioTrackList[mAudioTrackIndex] = new AudioTrack(AudioManager.STREAM_MUSIC,
                        audioSampleRate,
                        (audioChannels == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO),
                        AudioFormat.ENCODING_PCM_16BIT,
                        mInputBufferSize,
                        AudioTrack.MODE_STREAM);
                mAudioTrackList[mAudioTrackIndex].play();
                mAudioDecoder = MediaCodec.createDecoderByType(mime);
                mAudioDecoder.configure(format, null, null, 0);
                swLog.logi("mAudioDecoder configure" + format);
                mAudioDecoder.start();

                final ByteBuffer[] buffers = mAudioDecoder.getOutputBuffers();
                int sz = buffers[0].capacity();
                if (sz <= 0) {
                    sz = mInputBufferSize;
                }
                mAudioOutBuffers = new byte[sz];

                inputBuffers = mAudioDecoder.getInputBuffers();
                outputBuffers = mAudioDecoder.getOutputBuffers();
                swLog.logi("audio inputBuffers length is " + inputBuffers.length);
                swLog.logi("audio outputBuffers length is " + outputBuffers.length);
                return true;
            }
        }
        return false;
    }

    /**
     * 主事件循环
     */
    private synchronized void mainThreadProgress() throws InterruptedException {
        swLog.logv("mainThreadProgress mAudioCommStatus: " + mAudioCommStatus);
        switch (mAudioCommStatus) {
            case AUDIO_COMM_STATUS_IDLE:
                swLog.logv("AUDIO_COMM_STATUS_IDLE");
                break;
            case AUDIO_COMM_STATUS_END:
                swLog.logv("AUDIO_COMM_STATUS_END");
                break;
            case AUDIO_COMM_STATUS_ERROR:
                swLog.logv("AUDIO_COMM_STATUS_ERROR");
                break;
            case AUDIO_COMM_STATUS_INITIALIZED:
                swLog.logv("AUDIO_COMM_STATUS_INITIALIZED");
                break;
            case AUDIO_COMM_STATUS_PREPARED:
                swLog.logv("AUDIO_COMM_STATUS_PREPARED");
                break;
            case AUDIO_COMM_STATUS_PREPARING:
                swLog.logi("AUDIO_COMM_STATUS_PREPARING");
                try {
                    if (audioCodecInit()) {
                        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_PREPARED;
                    } else {
                        mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_PREPARE_ERROR;
                    }
                } catch (Exception e) {
                    mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_PREPARE_ERROR;
                }
                this.notify();
                break;
            case AUDIO_COMM_STATUS_STARTED:
            case AUDIO_COMM_STATUS_NEXT_STARTED:
                swLog.logv("AUDIO_COMM_STATUS_STARTED");
                pushInputBuffer();
            case AUDIO_COMM_STATUS_PAUSED:
            case AUDIO_COMM_STATUS_CODEC_END:
                getOutputBuffer();
                break;
            case AUDIO_COMM_STATUS_STOPPED:
                swLog.logv("AUDIO_COMM_STATUS_STOPPED");
                break;
            case AUDIO_COMM_STATUS_PLAYBACK_COMPLETED:
                swLog.logv("AUDIO_COMM_STATUS_PLAYBACK_COMPLETED");
                break;
        }
    }

    private void pushInputBuffer() {
        if (mAudioByteBufferQueue.size() > 10) {
            swLog.logv("mAudioByteBufferQueue full");
            return;
        }

        boolean isAudioEos = false;
        if (mAudioCommStatus == AudioCommStatus.AUDIO_COMM_STATUS_NEXT_STARTED) {
            isAudioEos = true;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            if (mAudioExtractor.getSampleSize() <= 0) {
                isAudioEos = true;
            }
        } else {
            swLog.logd("not support");
            return;
        }

        int inputIndex = mAudioDecoder.dequeueInputBuffer(1000);
        if (inputIndex < 0) {
            return;
        }

        ByteBuffer inputBuffer = inputBuffers[inputIndex];
        int sampleSize = mAudioExtractor.readSampleData(inputBuffer, 0);
        int sampleIndex = mAudioExtractor.getSampleTrackIndex();
        long presentationTimeUs = mAudioExtractor.getSampleTime() + mCodecStartTimeUs;
        swLog.logv("mAudioDecoder queueInputBuffer " + "sampleSize " + sampleSize +
                ", presentationTimeUs " + presentationTimeUs + ", sampleIndex " + sampleIndex +
                ", mCodecStartTimeUs " + mCodecStartTimeUs + ", " + mAudioExtractor.getSampleTime());

        if (!isAudioEos) {
            mAudioDecoder.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);
            mCodecLastTimeUs = presentationTimeUs;
            mAudioExtractor.advance();
        } else {
            swLog.logi("Audio onCodecCompletion");
            swLog.logi("Audio InputBuffer BUFFER_FLAG_END_OF_STREAM");
            mAudioDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_CODEC_END;
            if (mOnMediaRenderListener != null) {
                mOnMediaRenderListener.onCodecCompletion(mFilePath, mCodecLastTimeUs);
            }
        }
    }

    private void getOutputBuffer() {
        int outputBufferIndex = mAudioDecoder.dequeueOutputBuffer(info, 1000);
        switch (outputBufferIndex) {
            case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                swLog.logv("INFO_OUTPUT_FORMAT_CHANGED format : " + mAudioDecoder.getOutputFormat());
                break;
            case MediaCodec.INFO_TRY_AGAIN_LATER:
                swLog.logv("INFO_TRY_AGAIN_LATER");
                break;
            case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                outputBuffers = mAudioDecoder.getOutputBuffers();
                swLog.logv("INFO_OUTPUT_BUFFERS_CHANGED");
                break;
            default:
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                long presentationTimeUs = info.presentationTimeUs - mDecodeOffsetTimestampUs;
                long renderTimestampNs = presentationTimeUs * 1000 + mStartWhenNs;
                long delayMs = (renderTimestampNs - System.nanoTime()) / 1000_000;

                // All decoded frames have been rendered, we can stop playing now
                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    swLog.logd("OutputBuffer BUFFER_FLAG_END_OF_STREAM");
                    mAudioCommStatus = AudioCommStatus.AUDIO_COMM_STATUS_RENDER_END;
                }

                if (delayMs > 1000) {
                    swLog.logw("mAudioDecoder dequeueOutputBuffer presentationTimeUs " + presentationTimeUs +
                            ", size " + info.size + ", delayMs: " + delayMs +
                            ", mStartWhenNs: " + mStartWhenNs +
                            ", nanoTimeNs: " + (System.nanoTime() / 1000)
                    );
                } else {
                    swLog.logv("mAudioDecoder dequeueOutputBuffer presentationTimeUs " + presentationTimeUs +
                            ", size " + info.size + ", delayMs: " + delayMs +
                            ", mStartWhenNs: " + mStartWhenNs +
                            ", nanoTimeNs: " + (System.nanoTime() / 1000)
                    );
                }

                if (delayMs < 0) {
                    swLog.logi("drop this audio delayMs: " + delayMs + ", renderTimestampUs: " + presentationTimeUs + ", size " + info.size);
                    mAudioDecoder.releaseOutputBuffer(outputBufferIndex, false);
                    break;
                }

                if (info.size > 0) {
                    if (mAudioOutBuffers.length < info.size) {
                        mAudioOutBuffers = new byte[info.size];
                    }
                    outputBuffer.position(0);
                    outputBuffer.get(mAudioOutBuffers, 0, info.size);
                    outputBuffer.clear();
                    synchronized (mAudioByteBufferQueue) {
                        mAudioByteBufferQueue.add(new AudioByteBuffer(mAudioTrackIndex, mAudioOutBuffers, info.size, info.flags, presentationTimeUs, renderTimestampNs));
                        swLog.logv("mAudioByteBufferQueue add presentationTimeUs " + presentationTimeUs);
                    }
                }

                mAudioDecoder.releaseOutputBuffer(outputBufferIndex, false);
                break;
        }
    }

    private void trackWrite() {
        AudioByteBuffer audioByteBuffer = mAudioByteBufferQueue.peek();

        if (audioByteBuffer == null) {
            return;
        }

        long renderTimestampNs = audioByteBuffer.renderTimestampNs;
        long delayMs = (renderTimestampNs - System.nanoTime()) / 1000_000;

        if (delayMs > 1000) {
            swLog.logw("trackWrite delayMs: " + delayMs);
        }

        if (delayMs > 100) {
            return;
        }

        if (mAudioTrackList[audioByteBuffer.index] != null) {
            mAudioTrackList[audioByteBuffer.index].write(audioByteBuffer.buffer, 0, audioByteBuffer.buffer.length);
            if (mOnMediaRenderListener != null) {
                mOnMediaRenderListener.renderTimestampNs(renderTimestampNs, audioByteBuffer.presentationTimeUs);
            }
        }

        mAudioByteBufferQueue.remove();
        swLog.logv("mAudioByteBufferQueue remove presentationTimeUs " + audioByteBuffer.presentationTimeUs);
    }
}
